#include "stdafx.h"
#include "TpqMap.h"
#include "TpqMapManager.h"

namespace Tpq
{
	void TpqMap::LoadFile(CDC& renderDC, string filename)
	{
		this->filename = filename;
		inputFile.open(filename, ios_base::binary | ios_base::in);
		if (inputFile.fail())
			throw "Error opening " + filename;
		inputFile.read(reinterpret_cast<char*>(&header), sizeof(Header));
		if (inputFile.fail())
			throw "Error reading header from " + filename;

		// Validate file format.
		// Technically, we can deal with other formats except for elevation image, but we are just interested in
		// knowing what other parameters are there.
		if (header.magicNumber != 1)
			throw "Unrecognized file " + filename + ": magic number is not 0x00000001.";
		if (header.mapImageInfo.bitsPerPixel != 24 || header.mapImageInfo.pixelsPerEntity != 1)
			throw "Unrecognized file " + filename + ": map image bits per pixel is not 24 or pixels per entity is not 1.";
		if (header.elevationImageInfo.bitsPerPixel != 8 || header.elevationImageInfo.pixelsPerEntity != 2)
			throw "Unrecognized file " + filename + ": elevation image bits per pixel is not 8 or pixels per entity is not 2.";
		if (header.shadeImageInfo.bitsPerPixel != 8 || header.shadeImageInfo.pixelsPerEntity != 1)
			throw "Unrecognized file " + filename + ": shade image bits per pixel is not 8 or pixel per entity is not 1.";

		// TODO: Other levels?
		id.level	= 5;
		id.east		= (int)(header.east*3600);
		id.south	= (int)(header.south*3600);

		blockSizeWorld.width	= (header.east-header.west)*3600 / header.mapImageInfo.columns;
		blockSizeWorld.height	= (header.north-header.south)*3600 / header.mapImageInfo.rows;
		
		if (header.mapImageInfo.flags == 256)
		{
			blockSizeNative.width	= header.mapImageInfo.width;
			blockSizeNative.height	= header.mapImageInfo.height;
		}
		else
		{
			blockSizeNative.width	= header.mapImageInfo.width / header.mapImageInfo.columns;
			blockSizeNative.height	= header.mapImageInfo.height / header.mapImageInfo.rows;
		}

		nativeScale.first	= blockSizeNative.width/blockSizeWorld.width;
		nativeScale.second	= -blockSizeNative.height/blockSizeWorld.height;	// Always negative, since screen coordiate grows downwards.

		// Read offsets of map images, elevation images, and shade images.
		int mapImages = header.mapImageInfo.rows * header.mapImageInfo.columns;
		mapImageOffsets.resize(mapImages+1);
		inputFile.read(reinterpret_cast<char*>(mapImageOffsets.data()), sizeof(int)*(mapImages+1));

		int elevationImages = 2 * header.elevationImageInfo.rows * header.elevationImageInfo.columns;
		elevationImageOffsets.resize(elevationImages+1);
		elevationImageOffsets[0] = mapImageOffsets[mapImages];
		inputFile.read(reinterpret_cast<char*>(elevationImageOffsets.data()+1), sizeof(int)*elevationImages);
		
		int shadeImages = header.shadeImageInfo.rows * header.shadeImageInfo.columns;
		vector<int> shadeImageOffsets(shadeImages+1);
		shadeImageOffsets[0] = elevationImageOffsets[elevationImages];
		inputFile.read(reinterpret_cast<char*>(shadeImageOffsets.data()+1), sizeof(int)*shadeImages);

		// Render shade and map images
		Graphics renderGraphics(renderDC);
		
		GSize<int> mapImageSizeNative;
		mapImageSizeNative.width = blockSizeNative.width*header.mapImageInfo.columns;
		mapImageSizeNative.height = blockSizeNative.height*header.mapImageInfo.rows;

		GSize<int> shadeImageBlockSizeNative;
		shadeImageBlockSizeNative.width = mapImageSizeNative.width/header.shadeImageInfo.columns;
		shadeImageBlockSizeNative.height = mapImageSizeNative.height/header.shadeImageInfo.rows;

		nativeFullMap.reset(new Bitmap(mapImageSizeNative.width, mapImageSizeNative.height, &renderGraphics));

		Graphics g(nativeFullMap.get());
		/*g.SetInterpolationMode(InterpolationMode::InterpolationModeNearestNeighbor);
		g.SetPixelOffsetMode(PixelOffsetMode::PixelOffsetModeHalf);*/
		for (int row = 0; row < header.shadeImageInfo.rows; row ++)
			for (int col = 0; col < header.shadeImageInfo.columns; col ++)
			{
				MemoryBitmap bitmap = CreateBitmapFromFile(shadeImageOffsets[row*header.shadeImageInfo.columns+col],
					shadeImageOffsets[row*header.shadeImageInfo.columns+col+1] - shadeImageOffsets[row*header.shadeImageInfo.columns+col]);
				g.DrawImage(bitmap, RectF(col*shadeImageBlockSizeNative.width, row*shadeImageBlockSizeNative.height, shadeImageBlockSizeNative.width, shadeImageBlockSizeNative.height),
					0, 0, header.shadeImageInfo.width, header.shadeImageInfo.height, Unit::UnitPixel);
			}

		// Delay drawing the map images so that this function is more responsive.
		// Map images will be drawn on demand or on idle. Only the visible blocks will be drawn in a Draw() call.
		nativeMapletBitset.resize(header.mapImageInfo.columns * header.mapImageInfo.rows);
		nativeMapletsLoaded = 0;
	}

	void TpqMap::LoadElevationData()
	{
		GSize<int> elevationDimension;
		elevationDimension.width = header.elevationImageInfo.width * header.elevationImageInfo.columns;
		elevationDimension.height = header.elevationImageInfo.height * header.elevationImageInfo.rows;

		elevationData.resize(elevationDimension.height);
		for (vector<vector<short>>::iterator iterator = elevationData.begin(); iterator < elevationData.end(); iterator ++)
			iterator->resize(elevationDimension.width);

		for (int i = 0; i < header.elevationImageInfo.rows; i ++)
		{
			for (int j = 0; j < header.elevationImageInfo.columns; j ++)
			{
				MemoryBitmap bitmap1 = CreateBitmapFromFile(elevationImageOffsets[i*header.elevationImageInfo.columns+j], 
					elevationImageOffsets[i*header.elevationImageInfo.columns+j+1] - elevationImageOffsets[i*header.elevationImageInfo.columns+j]);
				MemoryBitmap bitmap2 = CreateBitmapFromFile(elevationImageOffsets[i*header.elevationImageInfo.columns+j+1], 
					elevationImageOffsets[i*header.elevationImageInfo.columns+j+2] - elevationImageOffsets[i*header.elevationImageInfo.columns+j+1]);

				BitmapData data1, data2;

				Rect rect(0, 0, header.elevationImageInfo.width, header.elevationImageInfo.height);
				Status status1 = bitmap1->LockBits(&rect, ImageLockMode::ImageLockModeRead, PixelFormat32bppARGB, &data1);
				Status status2 = bitmap2->LockBits(&rect, ImageLockMode::ImageLockModeRead, PixelFormat32bppARGB, &data2);
			
				if (status1 != Status::Ok || status2 != Status::Ok)
					throw "Failed to lock bits from elevation image in " + filename;
				
				int* p1 = reinterpret_cast<int*>(data1.Scan0);
				int* p2 = reinterpret_cast<int*>(data2.Scan0);
				for (int m = 0; m < header.elevationImageInfo.height; m ++)
				{
					for (int n = 0; n < header.elevationImageInfo.width; n ++)
					{
						short el = ((p1[n] & 0x00FF) << 8) | (p2[n] & 0x00FF);
						elevationData[header.elevationImageInfo.height*i+m][header.elevationImageInfo.width*j+n] = el;
					}
					p1 += data1.Stride/4;
					p2 += data1.Stride/4;
				}

				bitmap1->UnlockBits(&data1);
				bitmap2->UnlockBits(&data2);
			}
		}
	}

	void TpqMap::LoadAllNativeMaplets()
	{
		for (int row = 0; row < header.mapImageInfo.rows; row ++)
			for (int col = 0; col < header.mapImageInfo.columns; col ++)
				LoadNativeMaplet(row, col);
	}

	void TpqMap::LoadNativeMaplet(int row, int col)
	{
		int n = row*header.mapImageInfo.columns + col;

		if (!nativeMapletBitset[n])
		{
			Graphics g(nativeFullMap.get());

			ImageAttributes imageAttributes;
			static ColorMatrix matrix = 
			{ 
				{
					{1.0f,  0.0f,  0.0f,  0.0f, 0.0f},
					{0.0f,  1.0f,  0.0f,  0.0f, 0.0f},
					{0.0f,  0.0f,  1.0f,  0.0f, 0.0f},
					{0.0f,  0.0f,  0.0f,  0.8f, 0.0f},
					{0.0f,  0.0f,  0.0f,  0.0f, 1.0f}
				}
			};
			imageAttributes.SetColorMatrix(&matrix, ColorMatrixFlags::ColorMatrixFlagsDefault);

			MemoryBitmap bitmap = CreateBitmapFromFile(mapImageOffsets[n], mapImageOffsets[n+1]-mapImageOffsets[n]);
			g.DrawImage(bitmap, Rect(col*blockSizeNative.width, row*blockSizeNative.height, blockSizeNative.width, blockSizeNative.height),
				0, 0, blockSizeNative.width, blockSizeNative.height, UnitPixel, &imageAttributes);

			nativeMapletBitset[n] = true;
			nativeMapletsLoaded ++;
		}
	}

	bool TpqMap::HasPendingNativeMaplets()
	{
		return (nativeMapletsLoaded < header.mapImageInfo.columns*header.mapImageInfo.rows);
	}

	pair<double, double> TpqMap::GetNativeScale()
	{
		return nativeScale;
 	}

	void TpqMap::RenderMaplet(CDC& dc, RenderedMapletKey key, RenderedMaplet& maplet)
	{
		LoadNativeMaplet(key.row, key.column);

		double xScale = maplet.yScale/nativeScale.second * nativeScale.first;
		int width	= round(xScale * blockSizeWorld.width);
		int height	= round(-maplet.yScale * blockSizeWorld.height);

		CBitmap bitmap;
		maplet.bufferDC.reset(new CDC);
		maplet.bufferDC->CreateCompatibleDC(&dc);
		bitmap.CreateCompatibleBitmap(&dc, width, height);
		maplet.bufferDC->SelectObject(bitmap);
		Graphics g(*(maplet.bufferDC));

		g.DrawImage(nativeFullMap.get(), Rect(0, 0, width, height),
			key.column*blockSizeNative.width, key.row*blockSizeNative.height, blockSizeNative.width, blockSizeNative.height, Unit::UnitPixel);
	}

	MemoryBitmap TpqMap::CreateBitmapFromFile(int offset, int size)
	{
		MemoryBitmap bitmap(size);

		inputFile.seekg(offset, ios::beg);
		inputFile.read(bitmap.GetBuffer(), size);

		if (inputFile.fail())
			throw "Error reading from " + filename;

		bitmap.LoadBitmapFromBuffer();
		return bitmap;
	}

	void TpqMap::Draw(CDC& dc, GRect<double> rectWorld, Transformation worldToScreen, bool loadCacheOnly)
	{
		// Even if the scale is native, we want to render the block and cache. Its overall performance is
		// faster than just drawing the native image, based on profiling. Interesting. Memory usage is going
		// to be higher though, since we are basically duplicating the cache for native scale.
		int minCol = (int)floor((rectWorld.west - header.west*3600)/blockSizeWorld.width);
		int maxCol = (int)ceil((rectWorld.east - header.west*3600)/blockSizeWorld.width)-1;
		int minRow = (int)floor((header.north*3600 - rectWorld.north)/blockSizeWorld.height);
		int maxRow = (int)ceil((header.north*3600 - rectWorld.south)/blockSizeWorld.height)-1;

		for (int row = minRow; row <= maxRow; row ++)
			for (int col = minCol; col <= maxCol; col ++)
			{
				// Current grid boundary, in world coordinate system.
				GRect<double> gridBoundary;	
				gridBoundary.north	= header.north*3600 - blockSizeWorld.height * row;
				gridBoundary.south	= header.north*3600 - blockSizeWorld.height * (row+1);
				gridBoundary.west	= header.west*3600 + blockSizeWorld.width * col;
				gridBoundary.east	= header.west*3600 + blockSizeWorld.width * (col+1);

				// Drawing area within this grid, in world coordinate system.
				GRect<double> intersect = rectWorld & gridBoundary;	

				// Drawing area within this grid, in screen coordiante system.
				CRect screen;
				screen.left		= round(worldToScreen.xScale * intersect.west + worldToScreen.xDelta);
				screen.top		= round(worldToScreen.yScale * intersect.north + worldToScreen.yDelta);
				screen.right	= round(worldToScreen.xScale * intersect.east + worldToScreen.xDelta);
				screen.bottom	= round(worldToScreen.yScale * intersect.south + worldToScreen.yDelta);

				RenderedMapletKey key = { id, row, col };
				shared_ptr<RenderedMaplet> maplet = TpqMapManager::Instance().FindMapletInCache(key, worldToScreen.xScale, worldToScreen.yScale);
				if (maplet == nullptr)
				{
					// Create a new one and render it.
					maplet.reset(new RenderedMaplet);
					maplet->yScale = worldToScreen.yScale;

					RenderedMapletKey key = { id, row, col };
					RenderMaplet(dc, key, *maplet);

					// Then put it into the cache.
					TpqMapManager::Instance().PutRenderedMapletInCache(key, maplet);
				}
				else if (maplet->yScale != worldToScreen.yScale)
				{
					shared_ptr<RenderedMaplet> newItem(new RenderedMaplet);
					newItem->yScale = worldToScreen.yScale;

					RenderedMapletKey key = { id, row, col };
					
					if (loadCacheOnly)
					{
						RenderMaplet(dc, key, *newItem);
						TpqMapManager::Instance().PutRenderedMapletInCache(key, newItem);
					}
					else
						// Put it into the pending queue for later rendering
						TpqMapManager::Instance().PutPendingMapletInQueue(key, newItem);
				}

				if (loadCacheOnly)
					continue;

				// Update time stamp.
				maplet->lastAccessTime = clock();

				// World to local coordiante (within the grid) transformation.
				Transformation worldToMaplet;	
				worldToMaplet.xScale = maplet->yScale/nativeScale.second*nativeScale.first; // Maplet's aspect ratio is always the same as the native scale.
				worldToMaplet.yScale = maplet->yScale;
				worldToMaplet.xDelta = -worldToMaplet.xScale*gridBoundary.west;
				worldToMaplet.yDelta = -worldToMaplet.yScale*gridBoundary.north;

				// Rendering area in this grid, in maplet coordinate system.
				CRect rectMaplet;
				rectMaplet.left			= round(intersect.west * worldToMaplet.xScale + worldToMaplet.xDelta);
				rectMaplet.top			= round(intersect.north * worldToMaplet.yScale + worldToMaplet.yDelta);
				rectMaplet.right		= round(intersect.east * worldToMaplet.xScale + worldToMaplet.xDelta);
				rectMaplet.bottom		= round(intersect.south * worldToMaplet.yScale + worldToMaplet.yDelta);

				dc.StretchBlt(screen.left, screen.top, screen.Width(), screen.Height(), maplet->bufferDC.get(), 
					rectMaplet.left, rectMaplet.top, rectMaplet.Width(), rectMaplet.Height(), SRCCOPY);
			}
	}
}
